// Copyright 2007 Google, Inc. All Rights Reserved.

package com.google.jessewilson.codesearch.commandlinesearch;

import com.google.inject.Inject;
import com.google.jessewilson.codesearch.api.*;
import com.google.jessewilson.codesearch.api.spi.SearchProviderBackend;
import com.google.jessewilson.codesearch.io.IO;
import com.google.jessewilson.codesearch.util.ProducerSink;
import com.google.jessewilson.codesearch.util.StringToRegex;

import java.io.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * @author jessewilson
 */
class ClsSearchProviderBackend implements SearchProviderBackend {

  private final ClsConfiguration configuration;

  @Inject public ClsSearchProviderBackend(ClsConfiguration configuration) {
    this.configuration = configuration;
  }

  public ClsConfiguration getConfiguration() {
    return configuration;
  }

  public SearchResult search(Query query, ProducerSink<Line> sink, Page page)
      throws SearchProviderException {

    ResultParser parser = new ResultParser(this);

    int lineIndex = 0;
    boolean failuresHaveBeenLogged = false;
    BufferedReader reader = null;
    try {
      Process process = startProcess(query);
      reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
      for(String resultLine = reader.readLine(); resultLine != null; resultLine = reader.readLine()) {
        try {
          // skip results out of page range. Is there a better way?
          if (lineIndex < page.getStart() || lineIndex >= page.getEnd()) {
            continue;
          }

          // parse this page
          boolean lineWasSuccessful = parseAndAddLine(parser, resultLine, sink);

          // report only the first error in a request
          if (!lineWasSuccessful && !failuresHaveBeenLogged) {
            Logger.getLogger(ClsSearchProviderBackend.class.getName())
                .log(Level.WARNING, "Parse failures have occurred. "
                    + "Process is \"" + process.toString() + "\"");
            failuresHaveBeenLogged = true;
          }

        // use finally so that lineIndex is incremented when we continue
        } finally {
          lineIndex++;
        }
      }
    } catch (IOException e) {
      throw new SearchProviderException(e);
    } finally {
      IO.close(reader);
    }

    // prepare the result summary
    SearchResult result = new SearchResult();
    int totalResults = lineIndex + 1;
    result.setStartIndex(page.getStart());
    result.setTotalResults(totalResults);
    result.setHasMoreResults(totalResults >= page.getEnd());
    return result;
  }

  /**
   * Create a process to execute the specified query.
   */
  private Process startProcess(Query query) throws IOException {
    List<String> command = new ArrayList<String>();

    // prepare the process from the configuration
    command.add(configuration.getExecutable());
    String arguments = configuration.getArguments() != null ? configuration.getArguments() : "";
    // todo: escape quotes?
    command.addAll(Arrays.asList(arguments.split("\\s")));

    // prepare the process from the query
    ClsSearchProviderParameters queryExtra = (ClsSearchProviderParameters) query.getSearchProviderParameters();
    String processedSearchTerm = query.getSearchTerm();

    if (queryExtra != null) {
      for (String extraArgument : queryExtra.getExtraArguments().split("\\s")) {
        if (extraArgument.length() > 0) {
          command.add(extraArgument);
        }
      }
      if (queryExtra.convertToRegex()) {
        processedSearchTerm = new StringToRegex().regexify(processedSearchTerm);
      }
    }

    command.add(processedSearchTerm);

    return new ProcessBuilder(command).redirectErrorStream(true).start();
  }

  public SearchProvider getSearchProvider() {
    return new ClsSearchProvider();
  }

  /**
   * Move a single line of input into the result list.
   *
   * @return true if the line was processed successfully.
   */
  private boolean parseAndAddLine(ResultParser parser, String resultLine, ProducerSink<Line> sink) {
    try {
      sink.produced(parser.parse(resultLine));
      return true;
    } catch (ResultParser.ParseFailedException e) {
      // TODO(jessewilson): add an error to the queue
      Logger.getLogger(ClsSearchProviderBackend.class.getName())
          .log(Level.WARNING, e.getMessage(), e);
      return false;
    }
  }

  /**
   * Although not strictly necessary, this copies the source file to the local
   * filesystem.
   */
  public void downloadFile(CodeFile source, File target)
      throws SearchProviderException {
    try {
      InputStream in = new FileInputStream(source.getPath().toString());
      OutputStream out = new FileOutputStream(target);
      IO.pumpStreams(in, out);
    } catch (IOException e) {
      throw new SearchProviderException(e);
    }
  }

}
